using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using System.Collections.ObjectModel;
using Microsoft.Xna.Framework.Graphics;
using Perovich.GameObjects.Interfaces;

namespace Perovich.GameObjects
{
    /// <summary>
    /// Provides data for the <see cref="ComponentCollection"/> events.
    /// </summary>
    public class ComponentCollectionEventArgs : EventArgs
    {
        IGameComponent comp;
        /// <summary>
        /// Gets the component that was affected by this event.
        /// </summary>
        public IGameComponent GameComponent
        {
            get
            {
                return comp;
            }
        }

        /// <summary>
        /// Constructs a new instance of the <see cref="ComponentCollectionEventArgs"/> class.
        /// </summary>
        /// <param name="comp">The component that was affected.</param>
        public ComponentCollectionEventArgs(IGameComponent comp)
        {
            this.comp = comp;
        }
    }

    /// <summary>
    /// Provides managment of a collection of game components with additional sorting between three dimensional and two dimensional objects.
    /// </summary>
    public class ComponentCollection : IEnumerable<IGameComponent>
    {
        GameComponentCollection items2D;
        GameComponentCollection items3D;

        /// <summary>
        /// Constructs a new instance of the <see cref="ComponentCollection"/> type.
        /// </summary>
        public ComponentCollection()
        {
            items2D = new GameComponentCollection();
            items3D = new GameComponentCollection();
            items2D.ComponentAdded += new EventHandler<GameComponentCollectionEventArgs>(SignalComponentAdded);
            items3D.ComponentAdded += new EventHandler<GameComponentCollectionEventArgs>(SignalComponentAdded);
        }

        /// <summary>
        /// Gets the three dimensional components in this collection.
        /// </summary>
        public IEnumerable<IGameComponent> Get3D
        {
            get
            {
                foreach (var a in items3D)
                    yield return a;
            }
        }

        /// <summary>
        /// Gets the two dimensional components in this collection.
        /// </summary>
        public IEnumerable<IGameComponent> Get2D
        {
            get
            {
                foreach (var a in items2D)
                    yield return a;
            }
        }


        /// <summary>
        /// Gets the <see cref="IEnumerator{IGameComponent}"/> for this collection.
        /// </summary>
        /// <returns>An enumerator for this collection.</returns>
        public IEnumerator<IGameComponent> GetEnumerator()
        {
            foreach (var a in items3D)
                yield return a;
            foreach (var a in items2D)
                yield return a;
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            foreach (var a in items3D)
                yield return a;
            foreach (var a in items2D)
                yield return a;
        }

        /// <summary>
        /// Triggered when a component is added to this collection.
        /// </summary>
        public event EventHandler<ComponentCollectionEventArgs> ComponentAdded;

        private void SignalComponentAdded(object sender, GameComponentCollectionEventArgs e)
        {
            if (ComponentAdded != null)
                ComponentAdded(this, new ComponentCollectionEventArgs(e.GameComponent));
        }

        /// <summary>
        /// Adds a game component to this collection.
        /// </summary>
        /// <param name="gameObject">The component to add.</param>
        public void Add(IGameComponent gameObject)
        {
            if (gameObject is IAm3D)
            {
                items3D.Add(gameObject);
            }
            else
            {
                items2D.Add(gameObject);
            }
        }

        /// <summary>
        /// Initializes all the components in this collection.
        /// </summary>
        public void Initialize()
        {
            foreach (IGameComponent comp in this)
            {
                comp.Initialize();
            }
        }

        /// <summary>
        /// Updates all the components in this collection.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of the game's timing values.</param>
        public void Update(GameTime gameTime)
        {
            var sorted = this.OrderBy(o => (o as IUpdateable).UpdateOrder);
            foreach (IUpdateable comp in sorted)
            {
                if (comp.Enabled)
                    comp.Update(gameTime);
            }
        }

        /// <summary>
        /// Draws all the component's render targets in this collection.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of the game's timing values.</param>
        public void DrawOnTargets(GameTime gameTime)
        {
            var sorted = this.OrderBy(o =>
            {
                IDrawable dra = o as IDrawable;
                if (dra != null)
                    return dra.DrawOrder;
                else
                    return int.MinValue;
            });
            foreach (object comp in sorted)
            {
                DrawableComponent dra = comp as DrawableComponent;
                if ((dra != null) && (dra.Visible))
                {
                    dra.DrawOnTargets(gameTime);
                }
            }
        }

        /// <summary>
        /// Draws all the components in this collection.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of the game's timing values.</param>
        /// <param name="device">The graphics device used for drawing.</param>
        /// <param name="SBatches">The sprite batches to use.</param>
        public void Draw(GameTime gameTime, GraphicsDevice device, SpriteBatchCollection SBatches)
        {
            if (this.Get3D.Count() > 0)
            {
                device.BlendState = BlendState.Opaque;
                device.DepthStencilState = DepthStencilState.Default;
                device.SamplerStates[0] = SamplerState.LinearWrap;
                device.DepthStencilState = DepthStencilState.Default;

                var sorted = this.Get3D.OrderBy(o =>
                {
                    IDrawable dra = o as IDrawable;
                    if (dra != null)
                        return dra.DrawOrder;
                    else
                        return int.MinValue;
                });
                foreach (object comp in sorted)
                {
                    IDrawable dra = comp as IDrawable;
                    if ((dra != null) && (dra.Visible))
                    {
                        dra.Draw(gameTime);
                    }
                }
            }
            if (this.Get2D.Count() > 0)
            {
                SBatches.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend);

                var sorted = this.Get2D.OrderBy(o =>
                {
                    IDrawable dra = o as IDrawable;
                    if (dra != null)
                        return dra.DrawOrder;
                    else
                        return int.MinValue;
                });
                foreach (object comp in sorted)
                {
                    IDrawable dra = comp as IDrawable;
                    if ((dra != null) && (dra.Visible))
                    {
                        dra.Draw(gameTime);
                    }
                }

                SBatches.End();
            }
        }

        /// <summary>
        /// Disposes all the components in this collection.
        /// </summary>
        public void Dispose()
        {
            foreach (var comp in this)
            {
                IDisposable dis;
                if ((dis = (comp as IDisposable)) != null)
                    dis.Dispose();
            }
        }
    }
}
